Лабораторная работа №16 "Оценка производительности"
Материал для подготовки к лабораторной работе
Данная лабораторная работа будет полностью опираться на навыки, полученные в ходе выполнения лабораторных работ:
Цель
Дать количественную оценку, характеризующую производительность реализованной вычислительной системы. На текущий момент мы создали процессорную систему, которая способна взаимодействовать с внешним миром посредством периферийных устройств ввода-вывода и программатора, по сути являющуюся компьютером. Однако встает вопрос, какое место данная система занимает в ряду уже существующих вычислительных систем.
Для оценки производительности необходимо модифицировать существующую процессорную систему, а после собрать и запустить специализированное ПО, отвечающее за измерение производительности (будет использована программа Coremark).
Теория
Coremark — это набор синтетических тестов (специальных программ) для измерения производительности процессорной системы. В данный набор входят такие тесты, как работа со связными списками, матричные вычисления, обработка конечных автоматов и подсчет контрольной суммы. Результат выражается в одном числе, которое можно использовать для сравнения с результатами других процессорных систем.
Для подсчета производительности, coremark опирается на функцию, возвращающую текущее время, поэтому для оценки производительности нам потребуется вспомогательное периферийное устройство: таймер.
Для вывода результатов тестирования, необходимо описать способ, которым coremark сможет выводить очередной символ сообщения — для этого мы будем использовать контроллер UART из ЛР№13.
Кроме того, скомпилированная без оптимизаций программа будет занимать чуть более 32KiB, поэтому нам потребуется изменить размер памяти инструкций.
Таким образом, для того чтобы запустить данную программу, нам необходимо выполнить как аппаратные изменения процессорной системы (добавить таймер и (если отсутствует) контроллер UART), так и программные изменения самого coremark (для этого в нем предусмотрены специальные платформозависимые файлы, в которых объявлены функции, реализацию которых нам необходимо выполнить).
Говорят, что лучшей проверкой процессора на наличие ошибок является попытка запустить на нем ядро Linux. Наша процессорная система на это в принципе не рассчитана (поскольку для запуска Linux нужна поддержка нескольких дополнительных расширений), поэтому coremark можно по праву считать "бюджетным" аналогом проверки процессора на прочность.
Задание
- Реализовать модуль-контроллер "таймер".
- Подключить этот модуль к системной шине. 2.1. В случае, если до этого в ЛР13 вашим устройством вывода было не UART TX, вам необходимо подключить к системной шине готовый модуль uart_tx_sb_ctrl.
- Добавить реализацию платформозависимых функций программы coremark.
- Скомпилировать программу.
- Изменить размер памяти инструкций.
- Запустить моделирование.
- Сравнить результаты измерения производительности с результатами существующих процессорных системам.
Таймер
Разберемся с тем, как будет работать наш таймер. По сути, это просто системный счётчик (не путайте с программным счётчиком), непрерывно считающий такты с момента последнего сброса. Системным он называется потому, что работает на системной тактовой частоте. Значения частот, на которых работают процессорные системы сопоставимы с 32-битными значениями, поэтому системный счётчик должен быть 64-битным. Для измерения времени мы будем засекать значение счётчика на момент начала отсчета и значение счётчика в конце отсчёта. Зная тактовую частоту и разность между значениями счётчика мы с легкостью сможем вычислить прошедшее время. При этом нужно обеспечить счётчик такой разрядностью, чтобы он точно не смог переполниться.
Поскольку мы уже назвали данный модуль "таймером", чтобы тот не был слишком простым, давайте добавим ему функциональности: пускай это будет устройство, способное генерировать прерывание через заданное число тактов. Таким образом, процессорная система сможет засекать время без постоянного опроса счётчика. Для работы coremark эта функциональность не нужна — если ее реализация окажется слишком сложной для вас, просто создайте системный счётчик, инкрементирующийся каждый такт, с доступом на чтение по адресу 32'h0
.
Было бы удобно, чтобы мы могли управлять тем, каким образом данный модуль будет генерировать такое прерывание: однократно, заданное число раз, или же бесконечно, пока тот не остановят.
Таким образом, мы сформировали адресное пространство контроллера, представленное в таблице 1.
Адрес | Режим доступа | Допустимые значения | Функциональное назначение |
---|---|---|---|
0x00 | R | [0:2³²-1] | Значение младших 32 бит системного счётчика, доступное только для чтения |
0x04 | R | [0:2³²-1] | Значение старших 32 бит системного счётчика, доступное только для чтения |
0x08 | RW | [0:2³²-1] | Указание младших 32 бит задержки, спустя которую таймер будет генерировать прерывание |
0x0c | RW | [0:2³²-1] | Указание старших 32 бит задержки, спустя которую таймер будет генерировать прерывание |
0x10 | RW | [0:2] | Указание режима генерации прерываний (выключен, заданное число раз, бесконечно) |
0x14 | RW | [0:2³²-1] | Указание количества повторений генерации прерываний |
0x24 | W | 1 | Программный сброс |
_Таблица 1. Адресное пространство
Прототип модуля представлен в листинге 1.
module timer_sb_ctrl(
/*
Часть интерфейса модуля, отвечающая за подключение к системной шине
*/
input logic clk_i,
input logic rst_i,
input logic req_i,
input logic write_enable_i,
input logic [31:0] addr_i,
input logic [31:0] write_data_i,
output logic [31:0] read_data_o,
output logic ready_o,
/*
Часть интерфейса модуля, отвечающая за отправку запросов на прерывание
процессорного ядра
*/
output logic interrupt_request_o
);
Листинг 1. Прототип таймера.
Обратите внимание, что у модуля нет сигнала interrupt_return_i
. Модуль будет генерировать прерывания ровно на 1 такт. Если процессор в этот момент не будет готов обработать прерывания (обрабатывая в этот момент какой-либо другой перехват) — запрос будет сразу же пропущен и таймер начнет отсчитывать следующий.
Для работы данного контроллера потребуются следующие сигналы:
logic [63:0] system_counter;
logic [63:0] delay;
enum logic [1:0] {OFF, NTIMES, FOREVER} mode, next_mode;
logic [31:0] repeat_counter;
logic [63:0] system_counter_at_start;
system_counter
— регистр, ассоциированный с адресами0x00
(младшие 32 бита) и0x04
(старшие 32 бита), системный счётчик. Задача регистра заключается в ежетактном увеличении на единицу.delay
— регистр, ассоциированный с адресами0x08
(младшие 32 бита) и0x0c
(старшие 32 бита). Число тактов, спустя которое таймер (когда тот будет включен) сгенерирует прерывание. Данный регистр изменяется только сбросом, либо запросом на запись.mode
— регистр, ассоциированный с адресом0x10
. Режим работы таймера:OFF
— отключен (не генерирует прерывания)NTIMES
— включен до тех пор, пока не сгенерирует N прерываний (Значение N хранится в регистреrepeat_counter
и обновляется после каждого сгенерированного прерывания). После генерации N прерываний, переходит в режимOFF
.FOREVER
— бесконечная генерация прерываний. Не отключится, пока режим работы прерываний не будет изменен.
next_mode
— комбинационный сигнал, который подается на вход записи в регистрmode
(аналогnext_state
из ЛР№15). Данный сигнал меняется только запросами на запись по адресу0x10
или в случае, еслиrepeat_counter == 0
в режимеNTIMES
. Поскольку этому сигналу можно присваивать только значения сигналов такого же типа (timer_mods
), либо константы из перечисления, запросы на запись можно реализовать через блокcase
(где перебираются 3 возможных значенияwrite_data_i
).repeat_counter
— регистр, ассоциированный с адресом0x14
. Количество повторений для режимаNTIMES
. Уменьшается в момент генерации прерывания в этом режиме в случае, если еще не равен нулю.system_counter_at_start
— неархитектурный регистр, хранящий значение системного счётчика на момент начала отсчета таймера. Обновляется при генерации прерывания (если это не последнее прерывание в режимеNTIMES
) и при запросе на запись в регистрmode
значения неOFF
.
Выходной сигнал interrupt_request_o должен быть равен единице, если текущий режим работы не OFF
, а сумма system_counter_at_start
и delay
равна system_counter
.
Для подключения данного таймера к системной шине, мы воспользуемся первым свободным базовым адресом, оставшимся после ЛР13: 0x08
. Таким образом, для обращения к системному счётчику, процессор будет использовать адрес 0x08000000
для обращения к регистру delay
0x08000008
и т.п.
Настройка Coremark
В первую очередь, необходимо скачать исходный код данной программы, размещенный по адресу: https://github.com/eembc/coremark. На случай возможных несовместимых изменений в будущем, все дальнейшие ссылки будут даваться на слепок репозитория, который был на момент коммита d5fad6b
.
После этого, чтобы добавить поддержку нашей процессорной системы потребуется:
- Реализовать функцию, измеряющую время
- Реализовать функцию, выводящую очередной символ сообщения с результатами
- Реализовать функцию, выполняющую первичную настройку периферии перед тестом
- Выполнить мелкую подстройку, такую как количество итераций в тесте и указание аргументов, с которыми будет скомпилирована программа.
Все файлы, содержимое которых мы будем менять расположены в папке barebones.
1. Реализация функции, измеряющей время
Не мы первые придумали измерять время путем отсчета системных тактов, поэтому вся логика по измерению времени уже реализована в coremark. От нас требуется только реализовать функцию, которая возвращает текущее значение системного счётчика.
Данной функцией является barebones_clock
, расположенная в файле core_portme.c
. В данный момент, в реализации функции описан вызов ошибки (поскольку реализации как таковой нет). Мы должны заменить реализацию функции кодом, приведённым в листинге 2.
barebones_clock()
{
volatile ee_u32 *ptr = (ee_u32*)0x08000000;
ee_u32 tim = *ptr;
return tim;
}
Листинг 2. Код функции barebones_clock
.
После ЛР14 вы уже должны представлять, что здесь происходит. Мы создали указатель с абсолютным адресом 0x08000000
— адресом системного счётчика. Разыменование данного указателя вернет текущее значение системного счётчика, что и должно быть результатом вызова этой функции. Поскольку тест закончится менее чем за секунду, не обязательно загружать значение старших 32 бит (они будут не равны нулю только спустя 2³²тактов / 10⁶тактов/с ≈ 429c).
Для того, чтобы корректно преобразовать тики системного счётчика во время, используется функция time_in_secs
, которая уже реализована, но для работы которой нужно определить макрос CLOCKS_PER_SEC
, характеризующий тактовую частоту, на которой работает процессор. Давайте определим данный макрос сразу над макросом EE_TICKS_PER_SEC
:
#define CLOCKS_PER_SEC 10000000
На этом наша задача по измерению времени завершена. Остальные правки будут не сложнее этих.
2. Реализация вывода очередного символа сообщения
Для вывода очередного символа во встраиваемых системах используется (какое совпадение!) функция uart_send_char
, расположенная в файле ee_printf.c
.
В реализации данной функции вам уже предлагают алгоритм, по которому та должна работать. Необходимо:
- дождаться готовности UART к отправке;
- передать отправляемый символ;
- дождаться готовности UART к отправке (завершения передачи).
Давайте так и реализуем эту функцию:
uart_send_char(char c)
{
volatile ee_u8 *uart_ptr = (ee_u8 *)0x06000000;
while(*(uart_ptr+0x08));
*uart_ptr = c;
while(*(uart_ptr+0x08));
}
Листинг 3. Код функции uart_send_char_
.
0x06000000
— базовый адрес контроллера UART TX из ЛР13 (и адрес передаваемых этим контроллером данных).
0x08
— смещение до адреса регистра busy
в адресном пространстве этого контроллера.
3. Реализация функции первичной настройки
Это функция portable_init
, расположена в уже известном ранее файле [core_portme
.c]. Данная функция выполняет необходимые нам настройки перед началом теста. Для нас главное — настроить нужным образом контроллер UART.
Допустим мы хотим, чтобы данные передавались на скорости 115200
, c одним стоповым битом и контролем бита четности. В этом случае, мы должны добавить в начало функции следующий код:
portable_init(core_portable *p, int *argc, char *argv[])
{
volatile ee_u32 *uart_tx_ptr = (ee_u32 *)0x06000000;
*(uart_tx_ptr + 3) = 115200;
*(uart_tx_ptr + 4) = 1;
*(uart_tx_ptr + 5) = 1;
//...
}
Листинг 4. Код функции uart_send_char
.
4. Дополнительные настройки
Для тонких настроек используется заголовочный файл core_portme.h
, куда также требуется внести несколько изменений. Нам необходимо:
- Объявить в начале файла макрос
ITERATIONS
, влияющий на количество прогонов теста. Нам достаточно выставить значение 1. - Обновить значение макроса
COMPILER_FLAGS
, заменив его значениеFLAGS_STR
на"-march=rv32i_zicsr -mabi=ilp32"
, именно с этими аргументами мы будем собирать программу. Это опциональная настройка, которая позволит вывести флаги компиляции в итоговом сообщении. - Добавить подключение заголовочного файла
#include <stddef.h>
.
Компиляция
Для компиляции программы, вам потребуются предоставленные файлы Makefile и linker_script.ld, а также файл startup.S из ЛР№14. Эти файлы необходимо скопировать с заменой в корень папки с программой.
Makefile
написан из расчёта, что кросс-компилятор расположен по пути C:/riscv_cc/
. В случае, если это не так, измените первую строчку данного файла в соответствии с расположением кросс-компилятора.
Для запуска компиляции, необходимо выполнить следующую команду, находясь в корне программы coremark:
make
В случае, если на вашем рабочем компьютере не установлена утилита make
, то вы можете скомпилировать программу вручную, выполнив последовательность команд, приведённую в листинге 5.
export CC_BASE=/c/riscv_cc/bin/riscv-none-elf
export CC="$CC_BASE"-gcc
export OBJ_DUMP="$CC_BASE"-objdump
export OBJ_COPY="$CC_BASE"-objcopy
export SIZE="$CC_BASE"-size
export CC_FLAGS="-march=rv32i_zicsr -mabi=ilp32 -I./ -I./barebones"
export LD_FLAGS="-Wl,--gc-sections -nostartfiles -T linker_script.ld"
export OC_FLAGS="-O verilog --verilog-data-width=4"
$CC -c $CC_FLAGS -o core_main.o core_main.c
$CC -c $CC_FLAGS -o startup.o startup.S
$CC -c $CC_FLAGS -o core_list_join.o core_list_join.c
$CC -c $CC_FLAGS -o core_matrix.o core_matrix.c
$CC -c $CC_FLAGS -o core_state.o core_state.c
$CC -c $CC_FLAGS -o core_util.o core_util.c
$CC -c $CC_FLAGS -o core_portme.o barebones/core_portme.c
$CC -c $CC_FLAGS -o cvt.o barebones/cvt.c
$CC -c $CC_FLAGS -o ee_printf.o barebones/ee_printf.c
$CC $CC_FLAGS $LD_FLAGS *.o -o coremark.elf
$OBJ_DUMP -D coremark.elf > coremark_disasm.S
$OBJ_COPY $OC_FLAGS -j .data -j .bss coremark.elf coremark_data.mem
$OBJ_COPY $OC_FLAGS -j .text coremark.elf coremark_instr.mem
$SIZE coremark.elf
Листинг 5. Последовательность команд для компиляции coremark.
В случае успешной компиляции, вам будет выведено сообщение об итоговом размере секций инструкций и данных:
text data bss dec hex filename
34324 2268 100 36692 8f54 coremark.elf
Изменение размера памяти инструкций
Как видите, размер секции инструкций превышает 32KiB на 1556 байт (32768—34000). Поэтому на время оценки моделирования, нам придется увеличить размер памяти инструкций до 64KiB, изменив значение параметра INSTR_MEM_SIZE_BYTES
в пакете memory_pkg
до значения 32'h10000
. Размер памяти данных также необходимо увеличить, изменив значение параметра DATA_MEM_SIZE_BYTES
до 32'h4000
.
Обратите внимание, что увеличение размера памяти в 16 раз приведет к значительному увеличению времени синтеза устройства, поэтому данное изменение мы производим исключительно на время поведенческого моделирования.
Запуск моделирования
Программирование 32KiB по UART займет ощутимое время, поэтому вам предлагается проинициализировать память инструкций и данных "по-старинке" через системные функции $readmemh
.
Если все было сделано без ошибок, то примерно через 300ms
после снятия сигнала сброса с ядра процессора выход tx_o
начнет быстро менять свое значение, сигнализируя о выводе результатов программы, которые отобразятся в tcl console
примерно еще через 55ms
в виде листинга 6 (вывод сообщения будет завершен приблизительно на 355ms
времени моделирования).
2K performance run parameters for coremark.
CoreMark Size : 666
Total ticks : 2574834
Total time (secs): <скрыто до получения результатов моделирования>
Iterations/Sec : <скрыто до получения результатов моделирования>
ERROR! Must execute for at least 10 secs for a valid result!
Iterations : 1
Compiler version : GCC13.2.0
Compiler flags : -march=rv32i_zicsr -mabi=ilp32
Memory location : STACK
seedcrc : 0x29f4
[0]crclist : 0x7704
[0]crcmatrix : 0x1fd7
[0]crcstate : 0x8e3a
[0]crcfinal : 0x7704
Errors detected
Листинг 6. Лог вывода результатов coremark. Значения "Total time (secs)" и "Iterations/Sec" скрыты до получения результатов моделирования.
Порядок выполнения задания
- Опишите таймер в виде модуля
timer_sb_ctrl
. - Проверьте модуль с помощью верификационного окружения, описанного в файле lab_16.tb_timer.sv.
- Интегрируйте модуль
timer_sb_ctrl
в процессорную систему.- Ко входу
rst_i
модуля подключите сигналcore_reset_o
программатора. Таким образом, системный счётчик начнет работать только когда память системы будет проинициализирована. - Сигнал прерывания этого модуля подключать не обязательно, т.к. coremark будет осуществлять чтение путем опроса системного счётчика, а не по прерыванию.
- Ко входу
- В случае, если до этого в Л№Р13 вашим устройством вывода было не UART TX, вам необходимо подключить к системной шине готовый модуль uart_tx_sb_ctrl.
- Получите исходники программы coremark. Для этого можно либо склонировать репозиторий, либо скачать его в виде архива.
- Добавьте реализацию платформозависимых функций программы coremark. Для этого в папке
barebones
необходимо:- в файле
core_portme.c
:- реализовать функцию
barebones_clock
, возвращающую текущее значение системного счётчика; - объявить макрос
CLOCKS_PER_SEC
, характеризующий тактовую частоту процессора; - реализовать функцию
portable_init
, выполняющую первичную инициализацию периферийных устройств до начала теста;
- реализовать функцию
- в файле
ee_printf.c
реализовать функциюuart_send_char
, отвечающую за отправку очередного символа сообщения о результате.
- в файле
- Добавьте с заменой в корень программы файлы Makefile, linker_script.ld и startup.S.
- Скомпилируйте программу вызовом
make
.- Если кросскомпилятор расположен не в директории
C:/riscv_cc
, перед вызовомmake
вам необходимо соответствующим образом отредактировать первую строчку вMakefile
. - В случае отсутствия на компьютере утилиты
make
, вы можете самостоятельно скомпилировать программу вызовом команд, представленных в разделе "Компиляция".
- Если кросскомпилятор расположен не в директории
- Временно измените размер памяти инструкций до 64KiB, а памяти данных до 16KiB, изменив значение параметров
INSTR_MEM_SIZE_BYTES
иDATA_MEM_SIZE_BYTES
в пакетеmemory_pkg
на32'h10_000
и32'h4_000
соответственно. - Проинициализируйте память инструкций и память данных файлами
coremark_instr.mem
иcoremark_data.mem
, полученными в ходе компиляции программы.- Память можно проинициализировать двумя путями: с помощью вызова системной функции
$readmemh
, либо же с помощью программатора. Однако имейте в виду, что инициализация памятей с помощью программатора будет достаточно долго моделироваться в виду большого объема программы. - В случае, если инициализация будет осуществляться посредством
$readmemh
, не забудьте удалить первую строчку со стартовым адресом из файла, инициализирующего память данных. - В случае, если инициализация будет осуществляться с помощью программатора, используйте вспомогательные вызовы
program_region
из пакетаbluster_pkg
, как это было сделано вlab_15_tb_system
. - В исходном виде тестбенч описан под инициализацию памяти посредством
$readmemh
.
- Память можно проинициализировать двумя путями: с помощью вызова системной функции
- Выполните моделирование системы с помощью модуля lab_16.tb_coremark.
- Результаты теста будут выведены приблизительно на
355ms
времени моделирования.
- Результаты теста будут выведены приблизительно на
Оценка производительности
Прочти меня после успешного завершения моделирования
Итак, вы получили сообщение, представленное в листинге 7.
2K performance run parameters for coremark.
CoreMark Size : 666
Total ticks : 2901822
Total time (secs): 0.290182
Iterations/Sec : 3.446111
ERROR! Must execute for at least 10 secs for a valid result!
Iterations : 1
Compiler version : GCC13.2.0
Compiler flags : -march=rv32i_zicsr -mabi=ilp32
Memory location : STACK
seedcrc : 0xe9f5
[0]crclist : 0xe714
[0]crcmatrix : 0x1fd7
[0]crcstate : 0x8e3a
[0]crcfinal : 0xe714
Errors detected
Листинг 7. Лог вывода результатов coremark.
Не обращайте внимание на строки "ERROR! Must execute for at least 10 secs for a valid result!" и "Errors detected". Программа считает, что для корректных результатов, необходимо крутить ее по кругу в течении минимум 10 секунд, однако по большей части это требование необходимо для более достоверного результата у систем с кэшем/предсказателями переходов и прочими блоками, которые могут изменить количество тактов на прохождение между итерациями. Наш однотактный процессор будет вести себя одинаково на каждом круге, поэтому нет смысла в дополнительном времени моделирования. Тем не менее, если вы захотите получить результаты, не содержащих сообщения об ошибках, измените число итераций в файле core_portme.h
до 45.
Нас интересует строка:
Iterations/Sec : 3.446111
Это и есть так называемый "кормарк" — метрика данной программы. Результат нашего процессора: ~3.45 кормарка.
Обычно, для сравнения между собой нескольких реализаций микроархитектур, более достоверной считается величина "кормарк / МГц", т.е. число кормарков, поделённое на тактовую частоту процессора, поскольку время прохождения теста напрямую зависит от тактовой частоты. Это значит, что чип с менее удачной микроархитектурной реализацией может выиграть по кормарку просто потому, что он был выпущен по лучшей технологии, позволяющей запустить его на больших частотах. Кормарк/МГц нормализует результаты, позволяя сравнивать микроархитектурные решения, не заботясь о том, на какой частоте был получен результат.
Более того, при сравнении с другими результатами, необходимо учитывать флаги оптимизации, которые использовались при компиляции программы, поскольку они также влияют на результат. Например, если собрать coremark с уровнем оптимизаций -O1
, результат нашей системы скакнёт до 11.23 кормарков, что всего лишь является следствием того, что программа стала меньше обращаться к памяти вследствие оптимизаций. Именно поэтому результаты coremark указываются вместе с опциями, с которыми тот был собран.
Мы не будем уходить в дебри темных паттернов маркетинга и вместо этого будет оценивать производительность в лоб: сколько кормарков в секунду смог прогнать наш процессор без каких-либо оптимизаций в сравнении с представленными результатами других систем вне зависимости от их оптимизаций.
Таблица опубликованных результатов находится по адресу: https://www.eembc.org/coremark/scores.php. Нам необходимо отсортировать эту таблицу по столбцу CoreMark
, кликнув по нему.
Мы получим следующий расклад:
На что мы можем обратить внимание? Ну, во-первых, мы видим, что ближайший к нам микроконтроллер по кормарку — это ATmega2560
с результатом 4.25
кормарка. Т.е. наш процессор по производительности схож с микроконтроллерами Arduino.
Есть ли здесь еще что-нибудь интересное? Посмотрим в верх таблицы, мы можем увидеть производителя Intel с их микропроцессором Intel 80286. Как написано на вики, данный микропроцессор был в 3-6 раз производительней Intel 8086, который соперничал по производительности с процессором Zilog Z80, который устанавливался в домашний компьютер TRS-80.
А знаете, с чем был сопоставим по производительности компьютер TRS-80? С бортовым компьютером Apollo Guidance Computer, который проводил вычисления и контролировал движение, навигацию, управлял командным и лунным модулями в ходе полётов по программе Аполлон.
Иными словами, мы разработали процессор, который приблизительно в 7-14 раз производительнее компьютера, управлявшего полётом космического корабля, который доставил человека на Луну!
Можно ли как-то улучшить наш результат? Безусловно. Мы можем улучшить его примерно на 5% изменив буквально одну строчку. Дело в том, что для простоты реализации, мы генерировали сигнал stall
для каждой операции обращения в память. Однако приостанавливать работу процессора было необходимо только для операций чтения из памяти. Если не генерировать сигнал stall
для операций типа store
, мы уменьшим время, необходимое на исполнение бенчмарка. Попробуйте сделать это сами.
Добавление умножителей, конвейеризация и множество других потенциальных улучшений увеличат производительность в разы.
Но это, как говорится, уже другая история.